/**
 * Класс, содержащий описание формы для рисования.
 * 
 * @param {Array, String} [paths] Пути, описывающие контур (см. {@link ictinus.drawItems.Item}). Это может быть либо массив путей, либо строка в формате SVG path data {@link http://www.w3.org/TR/SVG/paths.html#PathData}
 * @author Sergey Chikuyonok (sc@design.ru)
 * @copyright Art.Lebedev Studio (http://www.artlebedev.ru)
 * 
 * @include "/ictinus/src/ictinus.js"
 * @include "/ictinus/src/ictinus.drawItems.js"
 * @include "/ictinus/src/ictinus.Shape.js"
 * @include "/ictinus/src/ictinus.Shape.pathData.js"
 */
ictinus.Shape = function(paths){
	/** 
	 * Кэш, устанавливаемый иктинусом, для ускорения перерисовки
	 * @private 
	 */
	this._cache = {};
	
	/** 
	 * Сессионные параметры,устанавливаемые на время рисования формы
	 * @private 
	 */
	this._session = {};
	
	if (typeof(paths) == 'string') {
		paths = ictinus.Shape.pathData.parse(paths);
	}
	
	/** 
	 * Векторные пути, описывающие форму
	 * @private 
	 */
	this.paths = paths || [];
	
	/** Толщина обводки, в пикселях */
	this.strokeWidth = 0;
	
	/** 
	 * Цвет обводки в шестнадцатеричном формате
	 * @example
	 * #00ffcc 
	 */
	this.strokeColor = '#000000';
	
	/** 
	 * Цвет заливки в шестнадцатеричном формате
	 * @example
	 * #00ffcc
	 */
	this.fillColor = '';
	
	/** Цвет обводки в шестнадцатеричном формате
	 * @example
	 * #ff0000
	 * /
	this.fillColor = '';
	
	/** Сетка масшатбирования (аналог 9-grid scale из Adobe Flash) */
	this.scaleGrid = {
		x1: 0,
		x2: 0,
		y1: 0,
		y2: 0
	};
	
	// отступы
	this.paddingLeft = 0;
	this.paddingRight = 0;
	this.paddingTop = 0;
	this.paddingBottom = 0;
	
	/** 
	 * Область, в которой находится контент. Используется внешними механизмами
	 * для определения масштабирования (scaleX и scaleY) формы под новый 
	 * контент, а также для позиционирования контента относительно формы.<br>
	 * <b>В стадии разработки, пользоваться крайне нежелательно!</b>
	 */
	this.contentBox = {
		x: 0,
		y: 0,
		width: 0,
		height: 0
	};
	
	/** @private */
	this._scaleX = 1;
	/** @private */
	this._scaleY = 1;
}

/**
 * Рассчитывает координаты второй контрольной точки
 * для обеспечения сохранности касательной между двумя кривыми Безье. 
 * В качестве первой пары аргументов указывается точка, 
 * <i>относительно</i> которой нужно считать координаты, в качестве второй —
 * точка, <i>для</i> которой нужно считать координаты, третья пара — 
 * концевая точка, которой принадлежат обе контрольные точки 
 * @param {Number} cp1_x
 * @param {Number} cp1_y
 * @param {Number} cp2_x
 * @param {Number} cp2_y
 * @param {Number} anchor_x
 * @param {Number} anchor_y
 */
ictinus.Shape.calculateTangentPoint = function(cp1_x, cp1_y, cp2_x, cp2_y, anchor_x, anchor_y){
	var a = Math.atan2(cp1_y - anchor_y, cp1_x - anchor_x);
	var b = (a + Math.PI) % (Math.PI * 2);

	var catet_x = cp2_x - anchor_x;
	var catet_y = cp2_y - anchor_y;

	var hypot = Math.sqrt(catet_x * catet_x + catet_y * catet_y);

	return {x: Math.cos(b) * hypot + anchor_x, y: Math.sin(b) * hypot + anchor_y};
};

/**
 * Создает форму из JSON-объекта
 * @param {Object} obj
 * @return {ictinus.Shape}
 */
ictinus.Shape.fromJSON = function(obj){
	/** @return {Array} */
	function explode(str) {
		return (str || '').replace(/^\s+|\s+$/g, '').replace(/\s+/, ' ').split(' ');
	}
	
	var result = new ictinus.Shape(obj.paths);
	result.scaleX(obj.scaleX);
	result.scaleY(obj.scaleY);
	
	var props = 'strokeWidth,strokeColor,fillColor'.split(',');
	for (var i = 0; i < props.length; i++) 
		if (obj.hasOwnProperty(props[i])) 
			result[props[i]] = obj[props[i]];
	
	result.padding(obj.padding);
	
	var sg = explode(obj.scaleGrid);
	if (sg.length == 4) {
		result.scaleGrid.x1 = sg[0];
		result.scaleGrid.x2 = sg[1];
		result.scaleGrid.y1 = sg[2];
		result.scaleGrid.y2 = sg[3];
	}
	
	var cb = explode(obj.contentBox);
	if (cb.length == 4) {
		result.contentBox.x = cb[0];
		result.contentBox.y = cb[1];
		result.contentBox.width = cb[2];
		result.contentBox.height = cb[3];
	}
	
	return result;
};

ictinus.Shape.prototype = {
	/**
	 * Исправляет касательные в кривых Безье там, где это надо
	 * @param {Array} paths
	 */
	fixTangents: function(paths){
		if (!paths)
			throw new Error('No paths specified');
			
		var orig_first, orig_last, real_first, real_last;
		
		// находим начальную и конечную инструкцию у оригинала
		this.traverse(function(/* ictinus.drawItems.Item */ item){
			if (!item.prev())
				orig_first = item;
			else if (!item.next())
				orig_last = item;
		}, this.paths);
		
		// бегаем по переданным путям
		this.traverse(function(/* ictinus.drawItems.Item */ item, paths){
			if (!item.prev())
				real_first = item;
			else if (!item.next())
				real_last = item;
			
			if (
				item.typeOf('bezier') && 
				item.locked &&
				item.prev().typeOf('bezier')
			) {
				var prev_item = item.prev();
				var tan = ictinus.Shape.calculateTangentPoint(prev_item.cpx2, prev_item.cpy2, item.cpx1, item.cpy1, prev_item.x, prev_item.y);
				item.cpx1 = tan.x;
				item.cpy1 = tan.y;
			}
		}, paths);
		
		// если у оригинала совпадают начальные и конечные точки — зафиксируем их
		// для текущих путей
		if (orig_first.x == orig_last.x && orig_first.y == orig_last.y) {
			real_last.x = real_first.x; 
			real_last.y = real_first.y; 
		}
		
		return paths;
		
	},
	
	/**
	 * Бегает по всем инструкциям рисования в том порядке, в котором они должны
	 * быть нарисованы, и выполняет на каждой функцию <code>func</code>
	 * @param {Function} func Функция, выполняемая на каждой инструкции рисования (в качестве аргументов ей передается текущая инструкция рисования и значение итератора)
	 * @param {ictinus.drawTypes.Item[]} [paths] Набор инструкций рисования, по которым нужно бегать (по умолчанию бегаем по <code>this.paths</code>)
	 * @param {Function} [sort] Функция сортировки, которая определяет, в какой последовательности нужно перебирать элементы (аналог <code>Array.prototype.sort</code>)
	 */
	traverse: function(func, paths, sort){
		paths = paths || this.getPaths();
		
		if (sort) {
			var ar = paths.slice(0).sort(sort);
			for (var i = 0; i < ar.length; i++) {
				func(ar[i], i);
			}
			
		} else {
			// находим точку старта
			for (var i = 0; i < paths.length; i++) {
				if (paths[i] instanceof ictinus.drawItems.MoveTo) {
					var start = paths[i];
					var j = 0;
					do {
						func(start, j++);
					} while(start = start.next())
					
					break;
				}
			}
		}
		
	},
	
	/**
	 * Возвращает пути, описывающие форму. Важно помнить, что это не ссылка на
	 * private-свойство <code>this.paths</code>, а именно новый массив путей,
	 * которые были пересчитаны с учетом масштаба и сетки масштабирования
	 * @param {Boolean} [no_session] Нужно ли отдавать сессионные пути, если они есть?  
	 * @return {Array}
	 */
	getPaths: function(no_session){
		
		if (this._session.paths && !no_session) {
			/*
			 * В метода draw() отдали другие пути — вернем их
			 */
			return this.fixTangents(this._session.paths);
		}
		
		var result = [];
		
		var props = {
			x: ['x', 'cpx', 'cpx1', 'cpx2'],
			y: ['y', 'cpy', 'cpy1', 'cpy2']
		};
		
		/**
		 * Выполняет на каждом элементе объекта <code>obj</code> функцию 
		 * <code>func</code> и присваивает элементу объекта результат работы 
		 * функции
		 * @param {Object} obj
		 * @param {Function} func
		 * @param {String} token Для какого типа свойств (<b>x</b> или <b>y</b>) нужно выполнить маппинг
		 */
		function map(obj, func, token) {
			for (var i = 0; i < props[token].length; i++) {
				var p = props[token][i];
				if (obj.hasOwnProperty(p)) 
					obj[p] = func(obj[p]);
			}
		}
		
		var sx = this.scaleX();
		var sy = this.scaleY();
		
		// считаем, на сколько изменились габариты формы и сетки
		var bounds = this.getBounds(true);
		var dw = (bounds.maxX - bounds.minX) * (sx - 1);
		var dh = (bounds.maxY - bounds.minY) * (sy - 1);
		
		var mapFunc = {
			moveX: function(value){
				return value + dw;
			},
			
			moveY: function(value){
				return value + dh;
			},
			
			scaleX: function(value){
				return value * sx;
			},
			
			scaleY: function(value){
				return value * sy;
			}
		};
		
		if (!this.isEmptyGrid()) {
			// сетка масштабирования не пустая — считаем пути согласно ей
			
			// FIXME сам по себе метод работает не совсем правильно: не корректно масштабируются кривые Безье, если контрольне точки находятся в другой ячейке
			
			/*
			 * Сетка масштабирования по сути представляет собой матрицу 3х3, для
			 * каждой ячейки которой соответствует свой способ масштабирования:
			 * — для 11, 13, 31, 33 масштабирование не применяется
			 * — для 12, 32 масштабирование по горизонтали
			 * — для 21, 23 масштабирование по вертикали
			 * — для 22 масштабирование по горизонтали и вертикали
			 * 
			 * Алгоритм выглядит так: сначала все компоненты рисования 
			 * (ictinus.drawTypes) виртуально распихаем по этим ячейкам, затем 
			 * масштабировуем все координаты в нужных ячейках
			 */
			
			var grid = this.scaleGrid;
			
			/**
			 * Возвращает ячейку сетки масштабирования (11, 31, 22 и т.д.), 
			 * в которой находится точка с указанными координатами
			 * @param {Number} x
			 * @param {Number} y
			 * @return {Number}
			 */
			function getCell(x, y) {
				var row, col;
				
				if (x <= grid.x1)
					col = 1;
				else if (x >= grid.x2)
					col = 3;
				else
					col = 2;
				
				if (y <= grid.y1)
					row = 10;
				else if (y >= grid.y2)
					row = 30;
				else
					row = 20;
				return row + col;
			}
			
			this.traverse(function(/* ictinus.drawItems.Item */ item){
				var p = item.clone();
				
				// не забываем проставлять ссылки на предыдущий/следующий элемент
				if (result.length) {
					p.prev(result[result.length - 1]);
					result[result.length - 1].next(p);
				}
				
				var cell;
				
				for (var j = 0; j < props.x.length; j++) {
					var px =  props.x[j], py = props.y[j];
					
					if (p.hasOwnProperty(px) && p.hasOwnProperty(py)) {
						switch (getCell(p[px], p[py])) {
							case 12:
								p[px] = mapFunc.scaleX(p[px]);
								break;
							case 13:
								p[px] = mapFunc.moveX(p[px]);
								break;
							case 21:
								p[py] = mapFunc.scaleY(p[py]);
								break;
							case 22:
								p[px] = mapFunc.scaleX(p[px]);
								p[py] = mapFunc.scaleY(p[py]);
								break;
							case 23:
								p[px] = mapFunc.moveX(p[px]);
								p[py] = mapFunc.scaleY(p[py]);
								break;
							case 31:
								p[py] = mapFunc.moveY(p[py]);
								break;
							case 32:
								p[px] = mapFunc.scaleX(p[px]);
								p[py] = mapFunc.moveY(p[py]);
								break;
							case 33:
								p[px] = mapFunc.moveX(p[px]);
								p[py] = mapFunc.moveY(p[py]);
								break;
						}
					}
				}
				
				result.push(p);
			}, this.paths);
			
		} else {
			// просто масштабируем все точки
			this.traverse(function(/* ictinus.drawItems.Item */ item){
				var p = item.clone();
				
				// не забываем проставлять ссылки на предыдущий/следующий элемент
				if (result.length) {
					p.prev(result[result.length - 1]);
					result[result.length - 1].next(p);
				}
				
				map(p, mapFunc.scaleX, 'x');
				map(p, mapFunc.scaleY, 'y');
				result.push(p);
			}, this.paths);
		}
		
//		return result;
		return this.fixTangents(result);
	},
	
	/**
	 * Добавляет путь к описанию формы
	 * @param {ictunus.draw_type.line()} Один или несколько путей
	 */
//	XXX старый метод, пока не нужен
//	addPath: function(path){
//		for (var i = 0; i < arguments.length; i++) {
//			this.paths.push(arguments[i]);
//		}
//	},
	
	/**
	 * Устанавливает или возвращает масштаб по горизонтали
	 * @param {Number|String} [value] Новое значение масштаба
	 * @return {Number}
	 */
	scaleX: function(value){
		if (arguments.length) {
			this._scaleX = parseFloat(value, 10);
		}
		return this._scaleX;
	},
	
	/**
	 * Устанавливает или возвращает масштаб по вертикали
	 * @param {Number|String} [value] Новое значение масштаба
	 * @return {Number}
	 */
	scaleY: function(value){
		if (arguments.length) {
			this._scaleY = parseFloat(value, 10);
		}
		return this._scaleY;
	},
	
	/**
	 * Устанавливает или возвращает ширину
	 * @param {Number|String} [value] Новое значение ширины
	 * @return {Number}
	 */
	width: function(value){
		var bounds = this.getBounds(true);
		var width = bounds.maxX - bounds.minX;
		
		if (arguments.length) {
			this.scaleX(parseFloat(value, 10) / width);
		}
		
		return width * this.scaleX();
	},
	
	/**
	 * Устанавливает или возвращает высоту
	 * @param {Number|String} [value] Новое значение высоты
	 * @return {Number}
	 */
	height: function(value){
		var bounds = this.getBounds(true);
		var height = bounds.maxY - bounds.minY;
		
		if (arguments.length) {
			this.scaleY(parseFloat(value, 10) / height);
		}
		
		return height * this.scaleY();
	},
	
	/**
	 * Устанавливает сетку масштабирования
	 * @param {Number} x1
	 * @param {Number} x2
	 * @param {Number} y1
	 * @param {Number} y2
	 */
	setScaleGrid: function(x1, x2, y1, y2){
		this.scaleGrid.x1 = Math.min(x1, x2);
		this.scaleGrid.x2 = Math.max(x1, x2);
		this.scaleGrid.y1 = Math.min(y1, y2);
		this.scaleGrid.y2 = Math.max(y1, y2);
	},
	
	/**
	 * Проверяет, является ли сетка масштабирования пустой. Используется для 
	 * проверки необходимости применения такого масштабирования.
	 * @return {Boolean}
	 */
	isEmptyGrid: function(){
		var g = this.scaleGrid;
		return (!g.x1 && !g.x2 && !g.y1 && !g.y2);
	},
	
	/**
	 * Отрисовывет текущую форму. Если передан параметр <code>paths</code>, то
	 * именно этот массив инструкций по рисованию будет использоваться для 
	 * отрисовки, а не внутренний массив. Это полезно использовать, например, 
	 * при создании анимаций, когда нет простого и понятного способа сохранить
	 * оригинал формы и при этом учитывать сетку масшатбирования
	 * @param {Element} target В каком элементе рисовать
	 * @param {Array} [paths] Инструкции для рисования 
	 */
	draw: function (target, paths) {
		this._session.paths = paths;
		ictinus.draw(target, this);
		delete this._session.paths;
	},
	
	/**
	 * Украшает. Если передан параметр <code>paths</code>, то
	 * именно этот массив инструкций по рисованию будет использоваться для 
	 * отрисовки, а не внутренний массив. Это полезно использовать, например, 
	 * при создании анимаций, когда нет простого и понятного способа сохранить
	 * оригинал формы и при этом учитывать сетку масшатбирования
	 * @param {Element} img Картинка, которую нужно украсить
	 * @param {Array} [paths] Инструкции для рисования 
	 */
	decorate: function (img, paths) {
		this._session.paths = paths;
		ictinus.decorate(img, this);
		delete this._session.paths;
	},
	
	/**
	 * Устанавливает для формы рисования. Они нижны, как правило, при работе 
	 * с анимациями. Например, при анимации формы с помощью метода 
	 * <code>easeoutcubic</code> негкоторые точки могут выходить за пределы
	 * холста, что приведет к обрезанию формы. Для этого и нужен padding: 
	 * по сути, он добавляет дополнительный размер canvas-элементу, чтобы 
	 * нормально отображались токи, выходящие за пределы размера формы
	 * @param {String|Number} value Отступы в CSS-формате без указания единиц (например: '10', '20 5 30 10')
	 */
	padding: function(value){
		if (typeof(value) == 'number') {
			this.paddingLeft = this.paddingRight = this.paddingTop = this.paddingBottom = value;
		} else if(typeof(value) == 'string') {
			value = value.split(/\s+/);
			var parse = function(v){
				return parseInt(v, 10);
			}
			
			switch (value.length) {
				case 1:
					this.paddingLeft = this.paddingRight = this.paddingTop = this.paddingBottom = parse(value[0]);
					break;
				case 2:
					this.paddingTop = this.paddingBottom = parse(value[0]);
					this.paddingLeft = this.paddingRight = parse(value[1]);
					break;
				case 3:
					this.paddingTop = parse(value[0]);
					this.paddingLeft = this.paddingRight = parse(value[1]);
					this.paddingBottom = parse(value[2]);
					break;
				case 4:
					this.paddingTop = parse(value[0]);
					this.paddingRight = parse(value[1]);
					this.paddingBottom = parse(value[2]);
					this.paddingLeft = parse(value[3]);
					break;
			}
		}
	},
	
	/**
	 * Обновляет размер формы по габаритам элемента
	 * @param {HTMLElement} elem Элемент, под размеры которого нужно подогнать форму
	 */
	updateSizeBy: function(elem){
		if (this.contentBox.width && this.contentBox.height) {
			// определен контентный блок, обновляем размеры по нему
			this.scaleX(elem.offsetWidth / this.contentBox.width);
			this.scaleY(elem.offsetHeight / this.contentBox.height);
		} else {
			// обновлем размер по габаритам формы
			this.width(elem.offsetWidth);
			this.height(elem.offsetHeight);
		}
	},
	
	/**
	 * Возвращает габаритный прямоугольник текущей формы (с учетом масштабирования)
	 * @param {Boolean} [no_scale] Вернуть габаритный прямоугольник без учета масштабирования 
	 */
	getBounds: function(no_scale){
		var result = {
			minX: 0,
			minY: 0,
			maxX: 0,
			maxY: 0
		};
		
		this.traverse(function(/* ictinus.drawItems.Item */ item){
			var bounds = item.getBounds();
			result.minX = Math.min(result.minX, bounds.minX);
			result.minY = Math.min(result.minY, bounds.minY);
			result.maxX = Math.max(result.maxX, bounds.maxX);
			result.maxY = Math.max(result.maxY, bounds.maxY);
		}, (no_scale) ? this.paths : null);
		
		return result;
	},
	
	/**
	 * Возвращает форму в формате JSON (удобно для передачи и хранения формы)
	 * @return {Object}
	 */
	toJSON: function(){
		/** @return {String} */
		function join() {
			return Array.prototype.join.call(arguments, ' ');
		}
		
		var result = {};
		var props = 'strokeWidth,strokeColor,fillColor'.split(',');
		for (var i = 0; i < props.length; i++) {
			result[props[i]] = this[props[i]];
		}
		
		result.scaleX = this.scaleX();
		result.scaleY = this.scaleY();
		
		result.padding = join(this.paddingTop, this.paddingRight, this.paddingBottom, this.paddingLeft);
		result.contentBox = join(this.contentBox.x, this.contentBox.y, this.contentBox.width, this.contentBox.height);
		result.scaleGrid = join(this.scaleGrid.x1, this.scaleGrid.y1, this.scaleGrid.x2, this.scaleGrid.y2);
		
		var paths = [];
		for (var i = 0; i < this.paths.length; i++) {
			paths.push(this.paths[i].toString());
		}
		result.paths = paths.join('');
		
		return result;
	}
};